'''
Contains all the common model code.  This should be common across projects.
'''

import cherrypy
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import sqlalchemy.sql.expression
import simplejson

from errors import NotInitialized

engine = None
Session = None
_db = None
Base = declarative_base()

def to_dict(obj):
    d = {}
    d.update(obj.__dict__)
    del d['_sa_instance_state']
    return d
Base.to_dict = to_dict

def filter(query, where):
    '''Restrict what data comes back.
    
    query - sqlalchemy.orm.Query - the query object we are modifying.
    where - string | dict - the filter to apply
    
    NOTES: everything in the array denotes AND, multiple arrays denotes OR, 
           may want to add possibility of another level of array inside array for nested OR
    
    Example:
      [[
        {"field": "id", "op": "=", "values": [3]},
        {"field": "id", "op": "<", "values": [5]},
        {"field": "id", "op": "IS NULL", "values": []}
      ], [
        {"field": "id", "op": "IN", "values": [1,2,3,4]}
      ]]
    '''
    if isinstance(where, basestring):
        where = simplejson.loads(where)
    
    if not isinstance(where, list):
        return query
    
    #TODO: parse the query.
    or_parts = []
    for clause in where:
        
        and_parts = []
        for stmt in clause:
            #verify the column? No because if they mess up we probably should die.
            
            col = sqlalchemy.sql.expression.column(stmt['field'])
            
            #handle the operator
            op = stmt['op'].upper()
            part = None
            use_part = False
            
            if op in ('=', '!=', '<', '<=', '=>', '>', 'LIKE', 'ILIKE'):
                part = col.op(op)(stmt['values'][0])
                use_part = True
            if op == 'IN':
                part = col.in_(stmt['values'])
                use_part = True
            if op == 'NOT IN':
                part = not col.in_(stmt['values'])
                use_part = True
            if op == 'IS NULL':
                part = col == None
                use_part = True
            if op == 'IS NOT NULL':
                part = col != None
                use_part = True
            if op == 'BETWEEN':
                part = col.op(op)(stmt['values'][0:2])
                use_part = True
            
            if use_part:
                and_parts.append(part)
        
        if len(and_parts) > 1:
            or_parts.append(sqlalchemy.sql.expression.and_(*and_parts))
        elif len(and_parts) == 1:
            or_parts.append(and_parts[0])
            
    if len(or_parts) > 1:
        return query.filter(sqlalchemy.sql.expression.or_(*or_parts))
    elif len(or_parts) == 1:
        return query.filter(or_parts[0])
    else:
        return query
    

def create(conn_string):
    '''Creates the tables.
    
    create must be called before working with the database.
    '''
    global engine, Session
    engine = create_engine(conn_string)
    
    Session = sessionmaker(bind=engine)
    
    Base.metadata.create_all(bind=engine)
    
def init_cherrypy():
    def init_db():
        global Session
        cherrypy.serving.db = Session()
    
    def close_db():
        global Session
        if hasattr(cherrypy.serving, 'db'):
            cherrypy.serving.db.close()
    
    #create hooks that will create and destroy session at the correct places.
    cherrypy.request.hooks.attach('on_start_resource', init_db)
    cherrypy.request.hooks.attach('on_end_request', close_db)
    
    cherrypy.db = cherrypy._ThreadLocalProxy('db')
    
def getDB():
    '''Returns a common Session object.  
    
    First we attempt to get this from cherrypy.  If it does not exist there 
    then we see if a global object in this file exists.  If so we return it 
    otherwise we create it.
    '''
    global _db, Session, engine
    if hasattr(cherrypy, 'db'):
        return getattr(cherrypy, 'db')
    else:
        if _db:
            return _db
        else:
            if Session:
                _db = Session(bind=engine)
                return _db
            else:
                raise NotInitialized()
    